import httpx
import json
from typing import Optional, Dict, Any
from config import settings
import time


class WeChatService:
    """
    微信登录服务类
    处理微信小程序和公众号的登录认证
    """
    
    def __init__(self):
        # 微信小程序配置
        self.app_id = getattr(settings, 'wechat_app_id', '')
        self.app_secret = getattr(settings, 'wechat_app_secret', '')

        # 微信公众号配置
        self.public_app_id = getattr(settings, 'wechat_public_app_id', '')
        self.public_app_secret = getattr(settings, 'wechat_public_app_secret', '')

        # 微信API地址
        self.code2session_url = "https://api.weixin.qq.com/sns/jscode2session"
        # 公众号网页 OAuth2
        self.oauth_access_token_url = "https://api.weixin.qq.com/sns/oauth2/access_token"
        self.oauth_userinfo_url = "https://api.weixin.qq.com/sns/userinfo"
        # 旧的应用级 token 与用户信息（不用于登录，仅保留兼容场景）
        self.access_token_url = "https://api.weixin.qq.com/cgi-bin/token"
        self.userinfo_url = "https://api.weixin.qq.com/cgi-bin/user/info"
        # 稳定版 access_token 接口
        self.stable_access_token_url = "https://api.weixin.qq.com/cgi-bin/stable_token"
        self._public_token: Optional[str] = None
        self._public_token_expires_at: float = 0.0
        self._last_error_msg: str = ""

    async def get_public_access_token_cached(self) -> Optional[str]:
        """
        获取公众号全局access_token（带Redis缓存与自动刷新）

        Returns:
            access_token字符串，失败返回None
        """
        now = time.time()
        if self._public_token and self._public_token_expires_at > now + 5:
            return self._public_token

        params = {
            'grant_type': 'client_credential',
            'appid': self.public_app_id,
            'secret': self.public_app_secret
        }
        try:
            async with httpx.AsyncClient() as client:
                response = await client.get(self.access_token_url, params=params)
                result = response.json()
                if 'errcode' in result:
                    self._last_error_msg = result.get('errmsg', '未知错误')
                    print(f"获取access_token错误: {self._last_error_msg}")
                    return None
                token = result.get('access_token')
                expires_in = int(result.get('expires_in', 7200))
                if token:
                    # 预留buffer，避免恰好过期
                    self._public_token = token
                    self._public_token_expires_at = now + max(1, expires_in - 200)
                    self._last_error_msg = ""
                    return token
                return None
        except Exception as e:
            self._last_error_msg = str(e)
            print(f"获取access_token异常: {self._last_error_msg}")
            return None

    async def create_qr_scene(self, scene_id: int, expire_seconds: int = 300) -> Optional[Dict[str, Any]]:
        """
        生成带场景值的临时二维码

        Args:
            scene_id: 场景值（整数）
            expire_seconds: 过期时间，默认300秒

        Returns:
            包含ticket与二维码URL的字典
        """
        access_token = await self.get_public_access_token_cached()
        if not access_token:
            # 返回可读错误信息，便于上层接口透出问题
            return {"error": self._last_error_msg or "access_token获取失败"}
        url = f"https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={access_token}"
        payload = {
            "expire_seconds": expire_seconds,
            "action_name": "QR_SCENE",
            "action_info": {"scene": {"scene_id": scene_id}}
        }
        try:
            async with httpx.AsyncClient() as client:
                resp = await client.post(url, json=payload)
                data = resp.json()
                if 'errcode' in data:
                    self._last_error_msg = data.get('errmsg', '未知错误')
                    print(f"创建二维码错误: {self._last_error_msg}")
                    return {"error": self._last_error_msg}
                ticket = data.get('ticket')
                if not ticket:
                    self._last_error_msg = "二维码ticket缺失"
                    return {"error": self._last_error_msg}
                qr_url = f"https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={ticket}"
                return {"ticket": ticket, "qr_url": qr_url, "expire_seconds": data.get('expire_seconds', expire_seconds)}
        except Exception as e:
            self._last_error_msg = str(e)
            print(f"创建二维码异常: {self._last_error_msg}")
            return {"error": self._last_error_msg}

    async def get_public_user_info(self, openid: str) -> Optional[Dict[str, Any]]:
        """
        获取公众号关注者用户信息

        Args:
            openid: 用户openid

        Returns:
            用户信息字典，失败返回None
        """
        access_token = await self.get_public_access_token_cached()
        if not access_token:
            return None
        params = {
            'access_token': access_token,
            'openid': openid,
            'lang': 'zh_CN'
        }
        try:
            async with httpx.AsyncClient() as client:
                response = await client.get(self.userinfo_url, params=params)
                result = response.json()
                if 'errcode' in result:
                    # 可能因凭证过期，刷新后重试一次
                    if result.get('errcode') == 40001:
                        token = await self.get_public_access_token_cached()
                        if not token:
                            return None
                        params['access_token'] = token
                        response = await client.get(self.userinfo_url, params=params)
                        result = response.json()
                        if 'errcode' in result:
                            print(f"获取用户信息错误: {result.get('errmsg', '未知错误')}")
                            return None
                    else:
                        print(f"获取用户信息错误: {result.get('errmsg', '未知错误')}")
                        return None
                return {
                    'openid': result.get('openid'),
                    'nickname': result.get('nickname'),
                    'sex': result.get('sex'),
                    'province': result.get('province'),
                    'city': result.get('city'),
                    'country': result.get('country'),
                    'headimgurl': result.get('headimgurl'),
                    'unionid': result.get('unionid')
                }
        except Exception as e:
            print(f"获取用户信息异常: {str(e)}")
            return None

    async def send_custom_text_message(self, openid: str, content: str) -> bool:
        """
        调用微信客服消息接口向指定用户推送文本消息

        Args:
            openid: 接收消息的用户 openid
            content: 文本消息内容

        Returns:
            推送结果布尔值，True 为成功，False 为失败
        """
        # 首次使用缓存的公众号 access_token
        access_token = await self.get_public_access_token_cached()
        print(f"使用access_token: {access_token}")
        if not access_token:
            print("获取access_token失败，无法推送消息")
            return False
        url = f"https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token={access_token}"
        payload = {
            "touser": openid,
            "msgtype": "text",
            "text": {"content": content}
        }
        try:
            async with httpx.AsyncClient() as client:
                resp = await client.post(url, content=json.dumps(payload, ensure_ascii=False), headers={"Content-Type": "application/json"})
                data = resp.json()
                # 处理凭证失效（40001）时的重试逻辑，参考微信官方稳定版说明
                if data.get("errcode") == 40001:
                    stable = await self.get_stable_access_token(force_refresh=True)
                    if not stable or not stable.get("access_token"):
                        print(f"推送消息失败(凭证失效且稳定版获取失败): {data.get('errmsg')}")
                        return False
                    retry_url = f"https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token={stable['access_token']}"
                    retry_resp = await client.post(retry_url, content=json.dumps(payload, ensure_ascii=False), headers={"Content-Type": "application/json"})
                    retry_data = retry_resp.json()
                    if retry_data.get("errcode") != 0:
                        print(f"推送消息失败(稳定版重试): {retry_data.get('errmsg')}")
                        return False
                    return True
                if data.get("errcode") != 0:
                    print(f"推送消息失败: {data.get('errmsg')}")
                    return False
                return True
        except Exception as e:
            print(f"推送消息异常: {str(e)}")
            return False

    async def get_stable_access_token(self, force_refresh: bool = False) -> Optional[Dict[str, Any]]:
        """
        获取稳定版接口调用凭据（getStableAccessToken）

        Args:
            force_refresh: 是否强制刷新，默认False

        Returns:
            包含 access_token 与 expires_in 的字典，失败返回 None
        """
        payload = {
            "grant_type": "client_credential",
            "appid": self.public_app_id,
            "secret": self.public_app_secret,
            "force_refresh": bool(force_refresh),
        }
        try:
            async with httpx.AsyncClient() as client:
                resp = await client.post(self.stable_access_token_url, json=payload)
                data = resp.json()
                if "errcode" in data and data.get("errcode") != 0:
                    # 常见错误：40013 invalid appid, 40125 invalid appsecret
                    print(f"获取稳定版access_token错误: {data.get('errmsg', '未知错误')}")
                    return None
                token = data.get("access_token")
                if not token:
                    return None
                return {"access_token": token, "expires_in": data.get("expires_in", 7200)}
        except Exception as e:
            print(f"获取稳定版access_token异常: {str(e)}")
            return None
    
    async def code2session(self, js_code: str) -> Optional[Dict[str, Any]]:
        """
        通过微信小程序的code获取session_key和openid
        
        Args:
            js_code: 微信小程序登录时获取的code
            
        Returns:
            包含openid和session_key的字典，失败返回None
        """
        params = {
            'appid': self.app_id,
            'secret': self.app_secret,
            'js_code': js_code,
            'grant_type': 'authorization_code'
        }
        
        try:
            async with httpx.AsyncClient() as client:
                response = await client.get(self.code2session_url, params=params)
                result = response.json()
                
                if 'errcode' in result:
                    print(f"微信登录错误: {result.get('errmsg', '未知错误')}")
                    return None
                    
                return {
                    'openid': result.get('openid'),
                    'session_key': result.get('session_key'),
                    'unionid': result.get('unionid')
                }
        except Exception as e:
            print(f"微信登录请求异常: {str(e)}")
            return None

    async def oauth_access_token(self, code: str) -> Optional[Dict[str, Any]]:
        """
        通过公众号网页授权的 code 交换用户 access_token 与 openid
        
        Args:
            code: 网页授权回调返回的 code
        
        Returns:
            包含 access_token、openid、unionid(如有) 的字典，失败返回 None
        """
        params = {
            'appid': self.public_app_id,
            'secret': self.public_app_secret,
            'code': code,
            'grant_type': 'authorization_code'
        }

        try:
            async with httpx.AsyncClient() as client:
                resp = await client.get(self.oauth_access_token_url, params=params)
                result = resp.json()
                if 'errcode' in result:
                    print(f"OAuth2 交换 access_token 错误: {result.get('errmsg', '未知错误')}")
                    return None
                return {
                    'access_token': result.get('access_token'),
                    'openid': result.get('openid'),
                    'unionid': result.get('unionid'),
                    'scope': result.get('scope'),
                    'expires_in': result.get('expires_in')
                }
        except Exception as e:
            print(f"OAuth2 交换 access_token 异常: {str(e)}")
            return None

    async def oauth_userinfo(self, access_token: str, openid: str) -> Optional[Dict[str, Any]]:
        """
        使用用户 access_token 获取网页授权用户信息（适用于扫码登录）
        
        Args:
            access_token: 用户 access_token（非应用级 token）
            openid: 用户 openid
        
        Returns:
            用户信息字典，失败返回 None
        """
        params = {
            'access_token': access_token,
            'openid': openid,
            'lang': 'zh_CN'
        }

        try:
            async with httpx.AsyncClient() as client:
                response = await client.get(self.oauth_userinfo_url, params=params)
                result = response.json()
                if 'errcode' in result:
                    print(f"OAuth2 获取用户信息错误: {result.get('errmsg', '未知错误')}")
                    return None
                return {
                    'openid': result.get('openid'),
                    'nickname': result.get('nickname'),
                    'sex': result.get('sex'),
                    'province': result.get('province'),
                    'city': result.get('city'),
                    'country': result.get('country'),
                    'headimgurl': result.get('headimgurl'),
                    'unionid': result.get('unionid')
                }
        except Exception as e:
            print(f"OAuth2 获取用户信息异常: {str(e)}")
            return None
    
    async def get_access_token(self) -> Optional[str]:
        """
        获取微信公众号的access_token
        
        Returns:
            access_token字符串，失败返回None
        """
        params = {
            'grant_type': 'client_credential',
            'appid': self.app_id,
            'secret': self.app_secret
        }
        
        try:
            async with httpx.AsyncClient() as client:
                response = await client.get(self.access_token_url, params=params)
                result = response.json()
                
                if 'errcode' in result:
                    print(f"获取access_token错误: {result.get('errmsg', '未知错误')}")
                    return None
                    
                return result.get('access_token')
        except Exception as e:
            print(f"获取access_token异常: {str(e)}")
            return None
    
    async def get_user_info(self, access_token: str, openid: str) -> Optional[Dict[str, Any]]:
        """
        通过access_token和openid获取用户信息
        
        Args:
            access_token: 微信access_token
            openid: 用户的openid
            
        Returns:
            用户信息字典，失败返回None
        """
        params = {
            'access_token': access_token,
            'openid': openid,
            'lang': 'zh_CN'
        }
        
        try:
            async with httpx.AsyncClient() as client:
                response = await client.get(self.userinfo_url, params=params)
                result = response.json()
                
                if 'errcode' in result:
                    print(f"获取用户信息错误: {result.get('errmsg', '未知错误')}")
                    return None
                    
                return {
                    'openid': result.get('openid'),
                    'nickname': result.get('nickname'),
                    'sex': result.get('sex'),
                    'province': result.get('province'),
                    'city': result.get('city'),
                    'country': result.get('country'),
                    'headimgurl': result.get('headimgurl'),
                    'unionid': result.get('unionid')
                }
        except Exception as e:
            print(f"获取用户信息异常: {str(e)}")
            return None
    
    def validate_signature(self, signature: str, timestamp: str, nonce: str, token: str) -> bool:
        """
        验证微信服务器签名
        
        Args:
            signature: 微信加密签名
            timestamp: 时间戳
            nonce: 随机数
            token: 开发者设置的token
            
        Returns:
            验证结果
        """
        import hashlib
        
        try:
            # 将token、timestamp、nonce三个参数进行字典序排序
            tmp_arr = [token, timestamp, nonce]
            tmp_arr.sort()
            tmp_str = ''.join(tmp_arr)
            
            # sha1加密
            sha1 = hashlib.sha1()
            sha1.update(tmp_str.encode('utf-8'))
            hashcode = sha1.hexdigest()
            
            return hashcode == signature
        except Exception as e:
            print(f"签名验证异常: {str(e)}")
            return False


# 全局微信服务实例
wechat_service = WeChatService()


# 便捷函数
async def wechat_login(js_code: str) -> Optional[Dict[str, Any]]:
    """
    微信小程序登录
    
    Args:
        js_code: 微信小程序登录code
        
    Returns:
        登录结果
    """
    return await wechat_service.code2session(js_code)


async def get_wechat_user_info(openid: str) -> Optional[Dict[str, Any]]:
    """
    获取微信用户信息（兼容旧逻辑：使用应用级 access_token 获取关注者资料）
    
    Args:
        openid: 用户openid
        
    Returns:
        用户信息
    """
    access_token = await wechat_service.get_access_token()
    if not access_token:
        return None
        
    return await wechat_service.get_user_info(access_token, openid)


def validate_wechat_signature(signature: str, timestamp: str, nonce: str, token: str = "your_token") -> bool:
    """
    验证微信服务器签名
    
    Args:
        signature: 微信加密签名
        timestamp: 时间戳
        nonce: 随机数
        token: 开发者设置的token
        
    Returns:
        验证结果
    """
    return wechat_service.validate_signature(signature, timestamp, nonce, token)